#!/usr/bin/env python2.5

import glob
import re
import os 
import stat
import urllib
from wsgiref import simple_server

__all__ = ['Wiki', 'FilePersistence']

class WikiName:
    wiki_namespace_separator = '/'
    validity_expr = '(([A-Z][a-z]+)%s?){2,}' % wiki_namespace_separator
    validity_re = re.compile('^%s$' % validity_expr)
    wiki_name_syntax = r'(?P<wiki_name>\b%s\b)' % validity_expr
    wiki_name_syntax_re = re.compile(wiki_name_syntax)
    
    def __init__(self, name):
        self.full_name = name
        name_parts = name.split(WikiName.wiki_namespace_separator) 
        self.short_name = name_parts[len(name_parts) - 1]
        name_parts.remove(self.short_name)
        self.name_space = tuple(name_parts)
        
    def __str__(self):
        return self.short_name
    
    def __eq__(self, other):
        return other is not None and other.full_name==self.full_name
    
    def __hash__(self):
        return hash(self.full_name)

    def is_valid(self):
        return WikiName.validity_re.match(self.short_name) is not None
    
    def parse_wiki_names(self, from_text):
        return [page_match.groups()[0] for page_match in WikiName.wiki_name_syntax_re.finditer(from_text)]
    
    def matches_attribute(self, attribute_name):
        index = len(attribute_name)
        return self.short_name.startswith(attribute_name) and self.short_name[index:index+1].isupper()
    
class Tag:
    def __init__(self, element, content, *attribs):
        self.nested_tags = []
        self.element = element
        self.append(content)
        self.attribs = attribs
        
    def append(self, nested_tag):
        if not (nested_tag is None or nested_tag == ''):
            self.nested_tags.append(nested_tag)
            return nested_tag
            
    def most_recently_nested(self, tagname = None):
        tag = self.nested_tags[-1] if len(self.nested_tags) > 0 else None
        if tagname is None or (isinstance(tag, Tag) and tag.element == tagname):
            return tag
        return self.append(Tag(tagname, ''))
        
    def __str__(self):
        tag_start = '%s' % self.element
        for attrib in self.attribs:
            tag_start += ' %s' % attrib
        nested_content = ''.join([str(tag) for tag in self.nested_tags])
        if nested_content == '':
            return '' if self.element == 'p' else '<%s />' % tag_start
        return '<%s>%s</%s>' % (tag_start, nested_content, self.element)
    
    def __eq__(self, other):
        return str(other) == str(self)
    
class AttributeConverter:
    zero_to_nineteen_values = ['zero', 'one', 'two', 'three', 'four', 'five', 
                        'six', 'seven', 'eight', 'nine',
                        'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 
                        'sixteen', 'seventeen', 'eighteen', 'nineteen']
    tens_values = [None, None, 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']
    digits_re = re.compile('((\w+)thousand)?((\w+)hundred)?(and)?(\w+ty)?(\w+)?')
    
    def convert(self, wiki_name, attribute_name):
        attribute_string = wiki_name[len(attribute_name):]
        number_result = self.convert_to_number(attribute_string)
        if number_result is attribute_string:
            return self.convert_from_camel_case(attribute_string)
        return number_result
    
    def convert_to_number(self, string, no_recursion=False):
        if string is None:
            return None
        else:
            match = string.lower()
        try:
            return AttributeConverter.zero_to_nineteen_values.index(match)
        except ValueError:
            if no_recursion:
                raise
            digit_parts = AttributeConverter.digits_re.search(match).groups()
            thousand_part, hundred_part, tens_part, other_part = digit_parts[1], digit_parts[3], \
                                                                    digit_parts[5], digit_parts[6]
            value = 0
            if thousand_part is not None:
                value += (1000 * self.convert_to_number(thousand_part))
            if hundred_part is not None:
                value += (100 * self.convert_to_number(hundred_part))
            if tens_part is not None:
                value += (10 * AttributeConverter.tens_values.index(tens_part))
            if other_part is not None:
                try:
                    value += self.convert_to_number(other_part, no_recursion=True)
                except ValueError:
                    return string
            return value
        
    def convert_from_camel_case(self, attribute_string):
        result = attribute_string[1:]
        for char in result:
            if char.isupper(): 
                result = result.replace(char, ' %s' % char.lower())
        return '%s%s' % (attribute_string[:1], result)
    
class WikiSyntax:
    # note starting char for non_block_para added for match deteection to each split line
    modifiers=r'((sort|filter|sum)=(\w+))'
    attribute_syntax = r'((\w+)(\(%s(,%s)?(,%s)?\))?(\{\s*((\w+)\:)?\s*(.*)\})?)' % (modifiers,modifiers,modifiers)
    attribute_syntax_re = re.compile(attribute_syntax) # TODO: duplication
    wiki_syntax = '' \
        + r'(?P<heading>^(\s)*(\++.+)$)' \
        + r'|(?P<listitem>^(\s)*((\*|\#)+.+)$)' \
        + r'|(?P<table>^(\s)*(\|.+\|$))' \
        + r'|(?P<non_block_para>^\r)' \
        + r'|(?P<emphasis>((\'){2,3}(\S)*(\'){2,3}))' \
        + r'|(?P<attribute>(\@%s))' % attribute_syntax \
        + r'|%s' % WikiName.wiki_name_syntax \
        + ''
    wiki_syntax_re = re.compile(wiki_syntax)
    
    def __init__(self, page):
        self.html_tags = []
        self.page = page
        self.expansion_callback = None
        
    def append_tag(self, tag):
        self.html_tags.append(tag)
        
    def current_tag(self, tag_name = None):
        current_tag = self.html_tags[-1] if len(self.html_tags) else None
        if tag_name is None:
            return current_tag # can legitimately be None
        elif current_tag is not None and tag_name == current_tag.element:
            return current_tag
        new_tag = Tag(tag_name, '')
        self.append_tag(new_tag)
        return new_tag
    
    def heading_to_html(self, heading, match):
        heading = heading.strip()
        text = heading.strip('+')
        level = len(heading) - len(text) + 1
        self.append_tag(Tag('h%s' % level, text.strip()))
    
    def _append_listitem(self, tag, values):
        for value in values: 
            tag.append(Tag('li', value))
    
    def listitem_to_html(self, item, match):
        item = item.strip()
        if item[:1] == '*':
            tag_name = 'ul'
        elif item[:1] == '#':
            tag_name = 'ol'
        text = item.strip(item[:1])
        level = len(item) - len(text)
        tag = self.current_tag(tag_name)
        for i in range(1, level):
            tag = tag.most_recently_nested('li').most_recently_nested(tag_name)
        self.expansion_callback = lambda values: self._append_listitem(tag, values)
        expanded_text = self.expand(text.strip())
        if expanded_text and expanded_text != '':
            self.expansion_callback([expanded_text])
            
    def _append_tableitem(self, row_tag, values):
        append_row = False
        for value in values:
            if append_row:
                row_tag = self.current_tag('table').append(Tag('tr', ''))
            if type(value) is type([]):
                append_row = True
                for column_value in value:
                    self._append_tableitem(row_tag, [column_value])
            else:
                row_tag.append(Tag('td', value))
            
    def table_to_html(self, table, match):
        table_tag = self.current_tag('table')
        row_tag = Tag('tr', '')
        table_tag.append(row_tag)
        self.expansion_callback = lambda values: self._append_tableitem(row_tag, values)
        for col in table.strip().strip('|').split('|'):
            expanded_text = self.expand(col)
            if expanded_text and expanded_text != '':
                self.expansion_callback([expanded_text])
     
    def non_block_para_to_html(self, item, match):
        self.append_tag(Tag('p', ''))
    
    def emphasis_to_html(self, phrase, match):
        text = phrase.strip('\'')
        level = len(phrase) - len(text)
        tag = 'em' if level == 4 else 'strong'
        return str(Tag(tag, text))
    
    def modify_attribute_evaluation(self, result, attribute_expr):
        modifiers = {}
        modifiers[attribute_expr[4]] = attribute_expr[5]
        modifiers[attribute_expr[8]] = attribute_expr[9]
        modifiers[attribute_expr[12]] = attribute_expr[13]
        if 'filter' in modifiers:
            result = [page for page in result if page.wiki_name.matches_attribute(modifiers['filter'])]
        if 'sort' in modifiers:
            result.sort(cmp=PageComparator(modifiers['sort']).cmp)
        if 'sum' in modifiers:
            result = [self.evaluate_attribute(page, modifiers['sum'], force_iterable=False) for page in result]
            result = ['%s' % reduce(lambda x,y: (x or 0)+(y or 0), result)]
        return result
    
    def evaluate_attribute(self, instance, attribute_name, force_iterable = True):
        try:
            page_attribute = (hasattr(instance, 'attribute') and instance.attribute(attribute_name)) \
                            or getattr(instance, attribute_name)
            result = page_attribute() if callable(page_attribute) else page_attribute
        except AttributeError:
            result = None
        if force_iterable:
            return result if hasattr(result, '__iter__') else [result]
        return result
    
    def replace_attribute(self, attribute, subattribute):
        if subattribute:
            result = self.evaluate_attribute(attribute, subattribute, force_iterable=False)
            return str(result)
        return str(attribute)

    def attribute_to_html(self, attribute, match):
#        TODO: use match.groups() here instead
        attribute_expr = WikiSyntax.attribute_syntax_re.search(attribute.strip('@'))
        if not attribute_expr:
            return attribute
        attribute_expr = attribute_expr.groups()
        attribute, variable, variable_expr = (attribute_expr[1], attribute_expr[-2], attribute_expr[-1])
        result = self.evaluate_attribute(self.page, attribute)
        result = self.modify_attribute_evaluation(result, attribute_expr)
        content, names = [], []
        for item in result:
            if not self.expansion_callback and len(content) > 0: 
                content.append(', ')
            if variable and variable_expr:
                expanded_parts = []
                for subexpr in variable_expr.split(','):
                    expr_parts = subexpr.split('=')
                    name, value = (expr_parts[0], expr_parts[1]) if len(expr_parts) > 1 else (None, expr_parts[0])
                    expanded_item = self.expand(re.sub('((%s)(\.(\w+))?)' % variable, 
                                       lambda match: self.replace_attribute(item, match.groups()[3]), 
                                       value))
                    if name: 
                        if name not in names: names.append(name)
                        expanded_parts.append(expanded_item)
                    else:
                        content.append(expanded_item)
                if len(expanded_parts) > 0:
                    if names not in content: content.append(names)
                    content.append(expanded_parts)
            else:
                content.append(self.expand('%s' % item))
        if self.expansion_callback:
            self.expansion_callback(content)
            return ''
        return ''.join(content)
        
    def wiki_name_to_html(self, wiki_name, match):
        return str(Tag('a', wiki_name, 'href="/%s"' % wiki_name))
    
    def expand(self, text_part):
        return re.sub(WikiSyntax.wiki_syntax_re, self.replace, text_part)
    
    def replace(self, match):
        tokentype = match.lastgroup
        token = match.groupdict()[tokentype]
        return getattr(self, tokentype + '_to_html')(token, match)

    def to_html(self):
        for line in self.page.text.splitlines():
            line = '\r' + line # Extra char added for matching non-block-para
            content = self.expand(line)
            self.current_tag().append(content)
            self.expansion_callback = None
        html = ''.join([str(tag) for tag in self.html_tags])
        return html

#TODO: backlinks, css, proper templating
class View:
    ok_button = 'ok'
    cancel_button = 'cancel'
    page_text_area = 'page_text'
    edit_button = 'edit'
    rendering_keys = ('editable', 'noneditable')
    doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
    xhtml = 'xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"'
    content_type = '<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />'
    css = '''<style type="text/css">
        body {background: #ffffcc;
        font-family: verdana,arial,helvetica;}
        table {background-color: #6666ff;
        padding: 1px; margin: 10px;}
        tr:first-child {background-color: #9999ff;}
        tr {background-color: #ccccff;}
        td {padding:5px;}
        a {color: #660099;}
        a:visited {color:#990099;}
        a:hover {background-color: #cc99ff;}
        </style>'''

    def __init__(self, syntax = WikiSyntax):
        self.syntax = syntax
        self.already_rendered = {}
    
    def is_post_from_cancel_button(self, args):
        return args.has_key(View.cancel_button)

    def is_post_from_ok_button(self, args):
        return args.has_key(View.ok_button)
    
    def get_page_text(self, args):
        return args[View.page_text_area]
    
    def is_post_from_edit_button(self, args):
        return args.has_key(View.edit_button)

    def update_from_post(self, page, args):
        if self.is_post_from_cancel_button(args):
            return False
        elif self.is_post_from_ok_button(args):
            page.store(urllib.unquote_plus(self.get_page_text(args)))
        elif self.is_post_from_edit_button(args):
            return True
        return False
    
    def reset_view(self, page):
        if page in self.already_rendered:
            self.already_rendered.__delitem__(page) 
            
    def render(self, page, post_args):
        if (len(post_args) > 0 and self.update_from_post(page, post_args)) or page.is_new():
            renderer = self.render_editable
            key = View.rendering_keys[0]
        else:
            renderer = self.render_noneditable
            key = View.rendering_keys[1]
        if not page in self.already_rendered:
            self.already_rendered[page] = {key:renderer(page)}
        elif not key in self.already_rendered[page]:
            self.already_rendered[page][key] = renderer(page)
        return self.already_rendered[page][key]

    def render_editable(self, page): 
        return '''%s<html %s><head>%s<title>Edit: %s</title>%s</head><body>
            <h1>%s</h1>
            <form id="main_form" method="post" action="/%s">
            <div>
            <textarea id="edit_page_text" name="%s" lang="en" dir="ltr" cols="125" rows="25">%s</textarea>
            </div>
            <input id="ok_button" name="%s" value="OK" type="submit" />
            <input id="cancel_button" name="%s" value="Cancel" type="submit" />
            </form>
            </body></html>''' % (View.doctype, View.xhtml, View.content_type, page.wiki_name, View.css,
                                 page.wiki_name.full_name, page.wiki_name.full_name, 
                                 View.page_text_area, page.text, 
                                 View.ok_button, View.cancel_button)
    
    def render_noneditable(self, page): 
        return '''%s<html %s><head>%s<title>%s</title>%s</head><body>
            <h1>%s</h1>
            <form id="edit_form" method="post" action="/%s">
            %s
            <input id="edit_button" name="%s" value="Edit" type="submit" />
            </form>
            </body></html>''' % (View.doctype, View.xhtml, View.content_type, page.wiki_name, View.css,
                                 page.wiki_name.full_name, page.wiki_name.full_name, 
                                 self.syntax(page).to_html(), View.edit_button)
  
class PageComparator:
    def __init__(self, sort_attribute = 'Name'):
        self.sort_attribute = sort_attribute
        
    def cmp(self, x, y):
        if x == y:
            return 0
        x_val, y_val = x.attribute(self.sort_attribute), y.attribute(self.sort_attribute)
        return -1 if x_val < y_val else 1
    
class Page:
    comparator = PageComparator().cmp
    
    def __init__(self, wiki_name, wiki, attribute_converter = AttributeConverter()):
        self.wiki_name = wiki_name
        self.wiki = wiki
        self.text = ''
        self.referred_pages = []
        self.attribute_converter = attribute_converter
        
    def __str__(self):
        return self.attribute('Name')
    
    def attribute(self, attribute_name):
        if attribute_name == 'Name':
            return self.wiki_name.full_name
        matches = [self.attribute_converter.convert(page.wiki_name.short_name, attribute_name) 
                   for page in self.referred_pages if page.wiki_name.matches_attribute(attribute_name)]
        if len(matches) == 0:
            return None 
        elif len(matches) == 1:
            return matches[0]
        else:
            return matches
    
    def is_new(self):
        return self.text == ''
    
    def store_text_without_persistence(self, text):
        if text == self.text:
            return False
        previously_referred_to_pages = self.referred_pages
        self.text = text
        self.referred_pages = [self.wiki.get_page(page_name) for page_name in self.wiki_name.parse_wiki_names(text)]
        pages_to_notify = [page for page in previously_referred_to_pages if page not in self.referred_pages]
        pages_to_notify.extend(self.referred_pages)
        for page in pages_to_notify:
            page.change_notification(self)
        self.wiki.view.reset_view(self)
        return True

    def store(self, text):
        if self.store_text_without_persistence(text):
            self.wiki.persistence.persist(self.wiki_name, self.text)
    
    def change_notification(self, notifying_page):
        if self.text_uses_backlinks():
            self.wiki.view.reset_view(self)
    
    def text_uses_backlinks(self):
        return '@backlinks' in self.text
    
    def backlinks(self):
        backlinks = self.wiki.get_backlinks(self)
        backlinks.sort(cmp=Page.comparator)
        return backlinks
        
class NoPersistence:
    def load_wiki(self, wiki):
        pass
    
    def persist(self, wiki_name, page_text):
        pass

class FilePersistence:
    file_suffix = '.txt'
    
    def __init__(self, docbase=os.path.join(os.getcwd(), 'wikibase')):
        self.docbase = docbase

    def load_wiki(self, wiki):
        if not os.path.exists(self.docbase):
            raise Exception('Wikibase storage directory %s is not readable.' % self.docbase)
        print '%s using Wikibase storage directory %s' % (self.__class__, self.docbase)
        self.load_wiki_from_path(wiki, self.docbase)
        
    def load_wiki_from_path(self, wiki, path):
        for file_name in glob.iglob(os.path.join(path, '*')):
            if file_name.endswith(FilePersistence.file_suffix):
                self.load_page_from_file(wiki, file_name)
            elif stat.S_ISDIR(os.stat(file_name).st_mode):
                self.load_wiki_from_path(wiki, file_name)
        
    def load_page_from_file(self, wiki, file_name):
        print 'Loading %s...' % file_name
        file = open(file_name, 'r')
        page_text = None
        try:
            page_text = file.read()
        finally:
            file.close()
        self.load_page_into_wiki(wiki, self.get_wiki_name(file_name), page_text)
        
    def get_wiki_name(self, file_name):
        start = len(self.docbase) + len(os.pathsep)
        end = len(file_name) - len(FilePersistence.file_suffix)
        wiki_like_path = file_name[start:end]
        full_name = None
        for part in wiki_like_path.split(os.pathsep): 
            if full_name is None:
                full_name = part
            else:
                full_name += '%s%s' % (WikiName.wiki_namespace_separator, part) 
        return WikiName(full_name)
        
    def load_page_into_wiki(self, wiki, wiki_name, page_text):
        page = wiki.get_page(wiki_name.full_name)
        page.store_text_without_persistence(page_text)
    
    def get_file_name(self, wiki_name):
        path = self.docbase
        for part in wiki_name.name_space:
            path = os.path.join(path, part)
        return os.path.join(path, wiki_name.short_name + FilePersistence.file_suffix)

    def persist(self, wiki_name, page_text):
        file_name = self.get_file_name(wiki_name)
        dir_name = os.path.dirname(file_name)
        if not os.path.exists(dir_name):
            os.makedirs(dir_name)
        file = open(file_name, 'w')
        try:
            file.write(page_text)
        finally:
            file.close()
    
class Wiki:
    def __init__(self, syntax=WikiSyntax, persistence=NoPersistence()):
        self.pages = {}
        self.view = View(syntax)
        self.persistence = persistence
        self.number_converter = AttributeConverter()
        self.persistence.load_wiki(self)
        
    def __call__(self, environ, start_response):
        requested_page_name = environ['PATH_INFO'][1:]
        if requested_page_name == '':
            status = '307 Temporary Redirect' 
            response_headers = [('Location','/FrontPage')] # TODO: standard front page
            start_response(status, response_headers)
            return ''
        
        page = self.get_page(requested_page_name)
        status = '200 OK'
        response_headers = [('Content-type','text/html')]
        start_response(status, response_headers)
        
        args = {}
        if environ['REQUEST_METHOD']=='POST':
            args_length = int(environ['CONTENT_LENGTH'])
            for arg in environ['wsgi.input'].read(args_length).split('&'):
                key, value = arg.split('=')
                args[key] = value
        view = self.view.render(page, args)
        return view
    
    def get_page(self, requested_page_name):
        wiki_name = WikiName(requested_page_name)
        if not wiki_name.is_valid():
            # TODO: need a real page...
            return None
        elif self.pages.has_key(wiki_name):
            return self.pages[wiki_name]
        else:
            page = Page(wiki_name, self, self.number_converter)
            self.pages[wiki_name] = page
            return page
        
    def get_backlinks(self, for_page):
        backlinks = [page for page in self.pages.values() if for_page in page.referred_pages]
        return backlinks

def main():
    wiki = Wiki(persistence=FilePersistence())
    httpd = simple_server.make_server('', 8080, wiki)
    httpd.serve_forever()
    
if __name__ == '__main__': main()
